home *** CD-ROM | disk | FTP | other *** search
/ Personal Computer World 2009 February / PCWFEB09.iso / Software / Resources / Audio, Video & Photo / Songbird 0.7.0 / Songbird_0.7.0_windows-i686-msvc8.exe / components / sbLibrarySearchSuggester.js < prev    next >
Text File  |  2008-08-06  |  15KB  |  497 lines

  1. /**
  2. //
  3. // BEGIN SONGBIRD GPL
  4. // 
  5. // This file is part of the Songbird web player.
  6. //
  7. // Copyright(c) 2005-2008 POTI, Inc.
  8. // http://songbirdnest.com
  9. // 
  10. // This file may be licensed under the terms of of the
  11. // GNU General Public License Version 2 (the "GPL").
  12. // 
  13. // Software distributed under the License is distributed 
  14. // on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either 
  15. // express or implied. See the GPL for the specific language 
  16. // governing rights and limitations.
  17. //
  18. // You should have received a copy of the GPL along with this 
  19. // program. If not, go to http://www.gnu.org/licenses/gpl.html
  20. // or write to the Free Software Foundation, Inc., 
  21. // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  22. // 
  23. // END SONGBIRD GPL
  24. //
  25.  */
  26.  
  27. /**
  28.  * \file sbLibrarySearchSuggester.js
  29.  * Provides autocomplete suggestions based on distinct values for a property
  30.  * Originally based on the Mozilla nsSearchSuggestions.js implementation.
  31.  *
  32.  * The format of the searchparam attribute is the following:
  33.  *
  34.  *  property;libraryguid;defaultvalues;unit
  35.  *
  36.  *  - 'property' is a property id, such as http://songbirdnest.com/data/1.0#artistName
  37.  *  - 'libraryguid' is the guid of a library from which to get distinct values,
  38.  *    or no value to get from all libraries
  39.  *  - 'defaultvalues' is a comma separated list of additional default values
  40.  *    which are always matched against this input even if they are not part of
  41.  *    the distinct values set
  42.  *  - 'unit' is the unit into which the distinct values should be converted to
  43.  *    before being matched against the input and inserted in the suggestion
  44.  *    result set.
  45.  */ 
  46.  
  47. Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
  48. Components.utils.import("resource://app/jsmodules/sbProperties.jsm");
  49.  
  50. const Cc = Components.classes;
  51. const Ci = Components.interfaces;
  52. const Cr = Components.results;
  53.  
  54. const CONTRACTID = "@mozilla.org/autocomplete/search;1?name=library-distinct-properties";
  55. const DESCRIPTION = "Songbird Library Search Suggestions";
  56. const CID = Components.ID("{1ed101bc-a11c-4e03-83af-514672bd3a70}");
  57.  
  58. const XPCOM_SHUTDOWN_TOPIC              = "xpcom-shutdown";
  59.  
  60.  
  61. /**
  62.  * Map of properties to hard-coded default values
  63.  */
  64. var gDefaultValues= {};
  65. gDefaultValues[SBProperties.genre] = [
  66.   "Alternative", "Blues/R&B", "Books&Spoken", "Children's Music",
  67.   "Classical", "Comedy", "Country", "Dance", "Easy Listening", "World",
  68.   "Electronic", "Folk", "Hip Hop/Rap", "Holiday", "House", "Industrial",
  69.   "Jazz", "New Age", "Nerdcore", "Podcast", "Pop", "Reggae", "Religious",
  70.   "Rock", "Science", "Soundtrack", "Techno", "Trance", "Unclassifiable"
  71. ];
  72.  
  73.  
  74. /**
  75.  * AutoCompleteResult contains the results returned by the Suggest
  76.  * service - it implements nsIAutoCompleteResult and is used by the auto-
  77.  * complete controller to populate the front end.
  78.  * @constructor
  79.  */
  80. function AutoCompleteResult(searchString,
  81.                                    defaultIndex,
  82.                                    errorDescription,
  83.                                    results) {
  84.   this._searchString = searchString;
  85.   this._defaultIndex = defaultIndex;
  86.   this._errorDescription = errorDescription;
  87.   this._results = results;
  88. }
  89. AutoCompleteResult.prototype = {
  90.   /**
  91.    * The user's query string
  92.    * @private
  93.    */
  94.   _searchString: "",
  95.  
  96.   /**
  97.    * The default item that should be entered if none is selected
  98.    * @private
  99.    */
  100.   _defaultIndex: 0,
  101.  
  102.   /**
  103.    * The reason the search failed
  104.    * @private
  105.    */
  106.   _errorDescription: "",
  107.  
  108.   /**
  109.    * The list of words returned by the Suggest Service
  110.    * @private
  111.    */
  112.   _results: [],
  113.  
  114.   /**
  115.    * @return the user's query string
  116.    */
  117.   get searchString() {
  118.     return this._searchString;
  119.   },
  120.  
  121.   /**
  122.    * @return the result code of this result object, either:
  123.    *         RESULT_IGNORED   (invalid searchString)
  124.    *         RESULT_FAILURE   (failure)
  125.    *         RESULT_NOMATCH   (no matches found)
  126.    *         RESULT_SUCCESS   (matches found)
  127.    */
  128.   get searchResult() {
  129.     if (this._results.length > 0) {
  130.       return Ci.nsIAutoCompleteResult.RESULT_SUCCESS;
  131.     } else {
  132.       return Ci.nsIAutoCompleteResult.RESULT_NOMATCH;
  133.     }
  134.   },
  135.  
  136.   /**
  137.    * @return the default item that should be entered if none is selected
  138.    */
  139.   get defaultIndex() {
  140.     return this._defaultIndex;
  141.   },
  142.  
  143.   /**
  144.    * @return the reason the search failed
  145.    */
  146.   get errorDescription() {
  147.     return this._errorDescription;
  148.   },
  149.  
  150.   /**
  151.    * @return the number of results
  152.    */
  153.   get matchCount() {
  154.     return this._results.length;
  155.   },
  156.  
  157.   /**
  158.    * Retrieves a result
  159.    * @param  index    the index of the result requested
  160.    * @return          the result at the specified index
  161.    */
  162.   getValueAt: function(index) {
  163.     return this._results[index];
  164.   },
  165.  
  166.   /**
  167.    * Retrieves a comment (metadata instance)
  168.    * @param  index    the index of the comment requested
  169.    * @return          the comment at the specified index
  170.    */
  171.   getCommentAt: function(index) {
  172.     return "";
  173.   },
  174.  
  175.   /**
  176.    * Retrieves a style hint specific to a particular index.
  177.    * @param  index    the index of the style hint requested
  178.    * @return          the style hint at the specified index
  179.    */
  180.   getStyleAt: function(index) {
  181.     if (!this._results[index])
  182.       return null;  // not a category label, so no special styling
  183.  
  184.     if (index == 0)
  185.       return "suggestfirst";  // category label on first line of results
  186.  
  187.     return "suggesthint";   // category label on any other line of results
  188.   },
  189.  
  190.   /** 
  191.    * Retrieves an image url. 
  192.    * @param  index    the index of the image url requested 
  193.    * @return          the image url at the specified index 
  194.    */ 
  195.   getImageAt: function(index) { 
  196.     return ""; 
  197.   },
  198.      
  199.   /**
  200.    * Removes a result from the resultset
  201.    * @param  index    the index of the result to remove
  202.    */
  203.   removeValueAt: function(index, removeFromDatabase) {
  204.     this._results.splice(index, 1);
  205.   },
  206.  
  207.   /**
  208.    * Part of nsISupports implementation.
  209.    * @param   iid     requested interface identifier
  210.    * @return  this object (XPConnect handles the magic of telling the caller that
  211.    *                       we're the type it requested)
  212.    */
  213.   QueryInterface: function(iid) {
  214.     if (!iid.equals(Ci.nsIAutoCompleteResult) &&
  215.         !iid.equals(Ci.nsISupports))
  216.       throw Cr.NS_ERROR_NO_INTERFACE;
  217.     return this;
  218.   }
  219. };
  220.  
  221.  
  222.  
  223.  
  224.  
  225.  
  226. /**
  227.  * Implements nsIAutoCompleteSearch to provide suggestions based 
  228.  * on Songbird's state.
  229.  *
  230.  * To access this suggester set autocompletesearch="library-distinct-properties"
  231.  * on an autocomplete textbox.  See the search.xml binding for details.
  232.  *
  233.  * @constructor
  234.  */
  235. function LibrarySearchSuggester() {
  236.     var os = Cc["@mozilla.org/observer-service;1"]
  237.                .getService(Ci.nsIObserverService);
  238.     os.addObserver(this, XPCOM_SHUTDOWN_TOPIC, false);
  239. }
  240.  
  241. LibrarySearchSuggester.prototype = {
  242.   classDescription: DESCRIPTION,
  243.   classID:          Components.ID(CID),
  244.   contractID:       CONTRACTID,
  245.  
  246.   /**
  247.    * The object implementing nsIAutoCompleteObserver that we notify when
  248.    * we have found results
  249.    * @private
  250.    */
  251.   _listener: null,
  252.   _libraryManager: null,
  253.   _lastSearch: null,
  254.   _timer: null,
  255.   _distinctValues: null,
  256.   _cacheParam: null,
  257.  
  258.  
  259.   /**
  260.    * Notifies the front end of new results.
  261.    * @param searchString  the user's query string
  262.    * @param results       an array of results to the search
  263.    * @private
  264.    */
  265.   onSearchResult: function(searchString, results) {
  266.     if (this._listener) {
  267.       var result = new AutoCompleteResult(
  268.           searchString,
  269.           0,
  270.           "",
  271.           results);
  272.  
  273.       this._listener.onSearchResult(this, result);
  274.  
  275.       // Null out listener to make sure we don't notify it twice, in case our
  276.       // timer callback still hasn't run.
  277.       this._listener = null;
  278.     }
  279.   },
  280.  
  281.  
  282.   /**
  283.    * Initiates the search result gathering process. Part of
  284.    * nsIAutoCompleteSearch implementation.
  285.    *
  286.    * @param searchString    the user's query string
  287.    * @param searchParam     the search parameter
  288.    * @param previousResult  unused, a client-cached store of the previous
  289.    *                        generated resultset for faster searching.
  290.    * @param listener        object implementing nsIAutoCompleteObserver which
  291.    *                        we notify when results are ready.
  292.    */
  293.   startSearch: function(searchString, searchParam, previousResult, listener) {
  294.  
  295.     // If no property was specified, we can't perform a search, abort now
  296.     if (!searchParam ||
  297.         searchParam == "") {
  298.       // notify empty result, probably not needed but hey, why not.
  299.       this.onSearchResult(searchString, []);
  300.       return;
  301.     }
  302.       
  303.     // If there's an existing request, stop it
  304.     this.stopSearch();
  305.  
  306.     // remember the listener
  307.     this._listener = listener;
  308.     
  309.     // if we do not yet have the distinct values, or if they
  310.     // have been invalidated, get them again now.
  311.     if (!this._distinctValues ||
  312.         this._cacheParam != searchParam) {
  313.       
  314.       // remember current search param
  315.       this._cacheParam = searchParam;
  316.       
  317.       // discard previous results no matter what,
  318.       // we want to use the new data
  319.       previousResult = null;
  320.       
  321.       // start anew
  322.       this._distinctValues = {};
  323.  
  324.       // get the library manager if needed
  325.       if (!this._libraryManager) {
  326.         this._libraryManager = 
  327.           Cc["@songbirdnest.com/Songbird/library/Manager;1"]
  328.             .getService(Ci.sbILibraryManager);
  329.       }
  330.  
  331.       // parse search parameters
  332.       var params = searchParam.split(";");
  333.              //Components.utils.reportError(params);
  334.  
  335.       this._prop = params[0];
  336.       var guid = params[1];
  337.       var additionalValues = params[2];
  338.       this._conversionUnit = params[3];
  339.  
  340.       // Record distinct values for a library
  341.       function getDistinctValues(aLibrary, prop, obj) {
  342.         if (!aLibrary) 
  343.           return;
  344.         var values = aLibrary.getDistinctValuesForProperty(prop);
  345.         while (values.hasMore()) { 
  346.           // is there a way to assert a key without doing an assignment ?
  347.           obj._distinctValues[values.getNext()] = true;
  348.         }
  349.       }
  350.       
  351.       // If we have a guid in the params, get the distinct values
  352.       // from a library with that guid, otherwise, get them from
  353.       // all libraries
  354.       if (guid && guid.length > 0) {
  355.         getDistinctValues(this._libraryManager.getLibrary(guid), this._prop, this);
  356.       } else {
  357.         var libs = this._libraryManager.getLibraries();
  358.         while (libs.hasMoreElements()) {
  359.           getDistinctValues(libs.getNext(), this._prop, this);
  360.         }
  361.       }
  362.       
  363.       // If we have additional values, add them to the 
  364.       // distinct values array
  365.       if (additionalValues && additionalValues.length > 0) {
  366.         var values = additionalValues.split(",");
  367.         for each (var value in values) {
  368.           this._distinctValues[value] = true;
  369.         }
  370.       }
  371.       
  372.       // Add hardcoded values, if any
  373.       this._addDefaultDistinctValues();
  374.       
  375.       // set this cache to expire in 5s
  376.  
  377.       if (!this._timer)
  378.         this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
  379.  
  380.       this._timer.cancel();
  381.       this._timer.initWithCallback(this, 5000, this._timer.TYPE_ONE_SHOT);
  382.     }
  383.  
  384.     // our search is case insensitive
  385.     var search = searchString.toLowerCase();
  386.  
  387.     // matching function
  388.     function startsWith(aString, aPartial) {
  389.       return (!aPartial ||
  390.                aPartial == "" ||
  391.                aString.toLowerCase().slice(0, aPartial.length) == aPartial);
  392.     }
  393.  
  394.     var results = [];
  395.     
  396.     // if this is a narrowing down of the previous search,
  397.     // use the previousResults array, otherwise,
  398.     // use the full distinctValues array
  399.     
  400.     if (previousResult &&
  401.         startsWith(search, this._lastSearch)) {
  402.       for (var i = 0 ; i < previousResult.matchCount ; i++) {
  403.         var value = previousResult.getValueAt(i);
  404.         if (startsWith(value, search))
  405.           results.push(value);
  406.       }
  407.     } else {
  408.  
  409.       var converter = null;
  410.       if (this._conversionUnit && this._conversionUnit != "") {
  411.         var propertyManager = 
  412.           Cc["@songbirdnest.com/Songbird/Properties/PropertyManager;1"]
  413.             .getService(Ci.sbIPropertyManager);
  414.         var info = propertyManager.getPropertyInfo(this._prop);
  415.         converter = info.unitConverter;
  416.       }
  417.  
  418.       for (var value in this._distinctValues) {
  419.         if (converter) {
  420.           value = converter.convert(value, 
  421.                                     converter.nativeUnitId, 
  422.                                     this._conversionUnit, 
  423.                                     -1, -1 /* no min/max decimals */);
  424.         }
  425.         if (startsWith(value, search))
  426.           results.push(value);
  427.       }
  428.     }
  429.     
  430.     // remember the last search string, to see if 
  431.     // we can use previousResult next time.
  432.     this._lastSearch = search;
  433.     
  434.     // Notify the listener that we got results
  435.     this.onSearchResult(searchString, results);    
  436.   },
  437.   
  438.   // one shot timer notification method
  439.   notify: function(timer) {
  440.     this._lastSearch = null;
  441.     this._distinctValues = null;
  442.   },
  443.  
  444.   /**
  445.    * Ends the search result gathering process. Part of nsIAutoCompleteSearch
  446.    * implementation.
  447.    */
  448.   stopSearch: function() {
  449.     // Nothing to do since we return our searches immediately.
  450.   },
  451.  
  452.   /**
  453.    * Add hardcoded default values for the current property
  454.    */
  455.   _addDefaultDistinctValues: function() {
  456.     var defaults = gDefaultValues[this._prop];
  457.     if (defaults) {
  458.       for each (var value in defaults) {
  459.         this._distinctValues[value] = true;
  460.       }
  461.     }
  462.   },
  463.  
  464.   /**
  465.    * nsIObserver
  466.    */
  467.   observe: function SAC_observe(aSubject, aTopic, aData) {
  468.     switch (aTopic) {
  469.       case XPCOM_SHUTDOWN_TOPIC:
  470.         this.stopSearch();
  471.         this._libraryManager = null;
  472.         if (this._timer) {
  473.           this._timer.cancel();
  474.           this._timer = null;
  475.         }
  476.         var os = Cc["@mozilla.org/observer-service;1"]
  477.                    .getService(Ci.nsIObserverService);
  478.         os.removeObserver(this, XPCOM_SHUTDOWN_TOPIC);
  479.         break;
  480.     }
  481.   },
  482.  
  483.   /**
  484.    * Part of nsISupports implementation.
  485.    * @param   iid     requested interface identifier
  486.    * @return  this object (XPConnect handles the magic of telling the caller that
  487.    *                       we're the type it requested)
  488.    */
  489.   QueryInterface:
  490.     XPCOMUtils.generateQI([Ci.nsIAutoCompleteSearch,
  491.                            Ci.nsIObserver])
  492. };
  493.  
  494. function NSGetModule(compMgr, fileSpec) {
  495.   return XPCOMUtils.generateModule([LibrarySearchSuggester]);
  496. }
  497.